Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit 9496d3eae9fc42cb7fbc3daa7f1b10912c2f1ba4


Parents : 80b49e5
Author : Ivan <ivan@quad4.io>
Signature : Signature validation error
Date : 2026-05-26T14:19:24-05:00

feat(tests): update tests

Changes
Diff

diff --git a/tests/backend/test_blackhole_logic.py b/tests/backend/test_blackhole_logic.py
index c181bccd..398c1b9f 100644
--- a/tests/backend/test_blackhole_logic.py
+++ b/tests/backend/test_blackhole_logic.py
@@ -237,3 +237,39 @@ async def test_get_blackhole_list(mock_rns_minimal, temp_dir):
info = data["blackholed_identities"][ident_hash_bytes.hex()]
assert info["reason"] == "Spam"
assert info["source"] == (b"\x02" * 32).hex()
+
+
+def test_is_destination_blocked_by_identity_hash(mock_rns_minimal, temp_dir):
+ """Match blocked destinations when looked up by identity hash.
+
+ When an identity hash is passed to is_destination_blocked, any blocked
+ destination belonging to that identity should match.
+ """
+ with patch("meshchatx.meshchat.generate_ssl_certificate"):
+ app_instance = ReticulumMeshChat(
+ identity=mock_rns_minimal,
+ storage_dir=temp_dir,
+ reticulum_config_dir=temp_dir,
+ )
+
+ app_instance.database = MagicMock()
+
+ ident_hash = "i" * 32
+ dest_hash = "d" * 32
+
+ # Simulate: no direct block on the identity hash, no announce with
+ # destination_hash == ident_hash, but there IS an announce for the
+ # identity and its destination is blocked.
+ app_instance.database.misc.is_destination_blocked.side_effect = lambda h: (
+ h == dest_hash
+ )
+ app_instance.database.announces.get_announce_by_hash.return_value = None
+ app_instance.database.announces.get_announces_by_identity_hash.return_value = [
+ {"destination_hash": dest_hash, "identity_hash": ident_hash}
+ ]
+
+ assert app_instance.is_destination_blocked(ident_hash) is True
+
+ # Also verify a non-blocked identity returns False
+ app_instance.database.misc.is_destination_blocked.side_effect = lambda h: False
+ assert app_instance.is_destination_blocked(ident_hash) is False

diff --git a/tests/backend/test_interface_discovery.py b/tests/backend/test_interface_discovery.py
index 43d1009d..af7b0446 100644
--- a/tests/backend/test_interface_discovery.py
+++ b/tests/backend/test_interface_discovery.py
@@ -306,7 +306,9 @@ async def test_discovered_interfaces_respect_whitelist_and_blacklist(temp_dir):
interfaces = data["interfaces"]
assert len(interfaces) == 4
- allowed = [i for i in interfaces if i.get("is_allowed") and not i.get("is_blacklisted")]
+ allowed = [
+ i for i in interfaces if i.get("is_allowed") and not i.get("is_blacklisted")
+ ]
assert len(allowed) == 1
assert allowed[0]["name"] == "peer-good-1"
blocked = [i for i in interfaces if i.get("is_blacklisted")]

diff --git a/tests/backend/test_lxmf_reactions.py b/tests/backend/test_lxmf_reactions.py
index 31729200..62152ac2 100644
--- a/tests/backend/test_lxmf_reactions.py
+++ b/tests/backend/test_lxmf_reactions.py
@@ -6,20 +6,49 @@ from unittest.mock import MagicMock
import LXMF
from meshchatx.src.backend.lxmf_utils import (
+ FIELD_REACTION,
LXMF_APP_EXTENSIONS_FIELD,
+ REACTION_CONTENT,
+ REACTION_TO,
convert_db_lxmf_message_to_dict,
convert_lxmf_message_to_dict,
- lxmf_fields_are_columba_reaction,
+ lxmf_fields_are_reaction,
+ parse_lxmf_reaction_field_dict,
)
-def test_lxmf_fields_are_columba_reaction():
- assert lxmf_fields_are_columba_reaction({}) is False
- assert lxmf_fields_are_columba_reaction({16: {"reaction_to": "abc"}}) is True
- assert lxmf_fields_are_columba_reaction({16: {"reply_to": "x"}}) is False
+def test_lxmf_fields_are_reaction():
+ assert lxmf_fields_are_reaction({}) is False
+ assert lxmf_fields_are_reaction({16: {"reaction_to": "abc"}}) is False
+ target = bytes.fromhex("aa" * 16)
+ assert (
+ lxmf_fields_are_reaction(
+ {
+ FIELD_REACTION: {
+ REACTION_TO: target,
+ REACTION_CONTENT: "\U0001f44d".encode("utf-8"),
+ },
+ },
+ )
+ is True
+ )
-def test_convert_lxmf_message_to_dict_reaction_field_16():
+def test_parse_lxmf_reaction_field_dict():
+ target_hex = "aa" * 32
+ parsed = parse_lxmf_reaction_field_dict(
+ {
+ REACTION_TO: bytes.fromhex(target_hex),
+ REACTION_CONTENT: "\U0001f44d".encode("utf-8"),
+ },
+ "bb" * 32,
+ )
+ assert parsed["reaction_to"] == target_hex
+ assert parsed["reaction_emoji"] == "\U0001f44d"
+ assert parsed["reaction_sender"] == "bb" * 32
+
+
+def test_convert_lxmf_message_to_dict_reaction_field_40():
mock_msg = MagicMock(spec=LXMF.LXMessage)
mock_msg.hash = b"h" * 8
mock_msg.source_hash = b"s" * 8
@@ -37,10 +66,9 @@ def test_convert_lxmf_message_to_dict_reaction_field_16():
mock_msg.q = None
target = "a" * 32
mock_msg.get_fields.return_value = {
- LXMF_APP_EXTENSIONS_FIELD: {
- "reaction_to": target,
- "emoji": "\U0001f44d",
- "sender": "f" * 32,
+ FIELD_REACTION: {
+ REACTION_TO: bytes.fromhex(target),
+ REACTION_CONTENT: "\u2764\ufe0f".encode("utf-8"),
},
}
@@ -48,19 +76,17 @@ def test_convert_lxmf_message_to_dict_reaction_field_16():
assert out["is_reaction"] is True
assert out["reaction_to"] == target
- assert out["reaction_emoji"] == "\U0001f44d"
- assert out["reaction_sender"] == "f" * 32
- assert "app_extensions" in out["fields"]
- assert out["fields"]["app_extensions"]["reaction_to"] == target
+ assert out["reaction_emoji"] == "\u2764\ufe0f"
+ assert out["reaction_sender"] == mock_msg.source_hash.hex()
+ assert out["fields"]["reaction"]["reaction_to"] == target
def test_convert_db_lxmf_message_to_dict_reaction():
target = "aa" * 16
fields_obj = {
- "app_extensions": {
+ "reaction": {
"reaction_to": target,
- "emoji": "\u2764\ufe0f",
- "sender": "11" * 16,
+ "reaction_content": "\u2764\ufe0f",
},
}
row = {
@@ -92,7 +118,7 @@ def test_convert_db_lxmf_message_to_dict_reaction():
assert out["is_reaction"] is True
assert out["reaction_to"] == target
assert out["reaction_emoji"] == "\u2764\ufe0f"
- assert out["reaction_sender"] == "11" * 16
+ assert out["reaction_sender"] == "cd" * 16
def test_convert_lxmf_message_to_dict_non_reaction_field_16_reply_to():

diff --git a/tests/backend/test_lxmf_utils_extended.py b/tests/backend/test_lxmf_utils_extended.py
index eaee9a9d..29924cf4 100644
--- a/tests/backend/test_lxmf_utils_extended.py
+++ b/tests/backend/test_lxmf_utils_extended.py
@@ -289,7 +289,7 @@ def test_sidebar_preview_reaction_incoming_uses_peer_name():
row = {
"content": "",
"fields": json.dumps(
- {"app_extensions": {"reaction_to": "abc123", "emoji": "\U0001f44d"}},
+ {"reaction": {"reaction_to": "abc123", "reaction_content": "\U0001f44d"}},
),
"is_incoming": 1,
"source_hash": "b" * 32,
@@ -307,7 +307,7 @@ def test_sidebar_preview_reaction_outbound_from_self_is_you():
row = {
"content": "",
"fields": json.dumps(
- {"app_extensions": {"reaction_to": "abc123", "emoji": "\u2764\ufe0f"}},
+ {"reaction": {"reaction_to": "abc123", "reaction_content": "\u2764\ufe0f"}},
),
"is_incoming": 0,
"source_hash": me,
@@ -324,7 +324,7 @@ def test_sidebar_preview_prefers_non_empty_content():
row = {
"content": " hi ",
"fields": json.dumps(
- {"app_extensions": {"reaction_to": "abc123", "emoji": "\U0001f44d"}},
+ {"reaction": {"reaction_to": "abc123", "reaction_content": "\U0001f44d"}},
),
"is_incoming": 1,
}

diff --git a/tests/backend/test_media_sticker_reaction_gif_security.py b/tests/backend/test_media_sticker_reaction_gif_security.py
index 80c87c4d..7c15b074 100644
--- a/tests/backend/test_media_sticker_reaction_gif_security.py
+++ b/tests/backend/test_media_sticker_reaction_gif_security.py
@@ -123,7 +123,7 @@ def test_validate_gif_payload_accepts_minimal_gif():
assert len(h) == 64
-def _minimal_lxmf_mock(app_ext: dict):
+def _minimal_lxmf_mock_reaction(reaction_to: str, emoji: str):
m = MagicMock()
m.hash = os.urandom(16)
m.source_hash = os.urandom(16)
@@ -139,26 +139,30 @@ def _minimal_lxmf_mock(app_ext: dict):
m.rssi = None
m.snr = None
m.q = None
- m.get_fields.return_value = {lxmf_utils.LXMF_APP_EXTENSIONS_FIELD: app_ext}
+ target_bytes = None
+ try:
+ target_bytes = bytes.fromhex(reaction_to)
+ except ValueError:
+ target_bytes = reaction_to.encode("utf-8", errors="replace")
+ m.get_fields.return_value = {
+ lxmf_utils.FIELD_REACTION: {
+ lxmf_utils.REACTION_TO: target_bytes,
+ lxmf_utils.REACTION_CONTENT: emoji.encode("utf-8", errors="replace"),
+ },
+ }
return m
@settings(max_examples=80, deadline=None)
@given(
emoji=st.text(max_size=800, alphabet=st.characters(blacklist_categories=("Cs",))),
- reaction_to=st.text(
- max_size=800, alphabet=st.characters(blacklist_categories=("Cs",))
- ),
- sender=st.text(max_size=800, alphabet=st.characters(blacklist_categories=("Cs",))),
+ reaction_to=st.text(max_size=64, alphabet="0123456789abcdef"),
)
-def test_convert_lxmf_reaction_app_extensions_fuzzing(emoji, reaction_to, sender):
- app_ext = {
- "reaction_to": reaction_to,
- "emoji": emoji,
- "sender": sender,
- }
+def test_convert_lxmf_reaction_field_fuzzing(emoji, reaction_to):
+ if len(reaction_to) % 2 != 0:
+ reaction_to = "0" + reaction_to
out = lxmf_utils.convert_lxmf_message_to_dict(
- _minimal_lxmf_mock(app_ext),
+ _minimal_lxmf_mock_reaction(reaction_to, emoji),
include_attachments=False,
)
assert out["is_reaction"] is True

diff --git a/tests/backend/test_notification_user_facing_filter.py b/tests/backend/test_notification_user_facing_filter.py
index 607499c3..54a6d26a 100644
--- a/tests/backend/test_notification_user_facing_filter.py
+++ b/tests/backend/test_notification_user_facing_filter.py
@@ -28,7 +28,9 @@ from aiohttp import web
from aiohttp.test_utils import TestClient, TestServer
from meshchatx.src.backend.lxmf_utils import (
- LXMF_APP_EXTENSIONS_FIELD,
+ FIELD_REACTION,
+ REACTION_CONTENT,
+ REACTION_TO,
compute_lxmf_conversation_unread_from_latest_row,
is_user_facing_lxmf_payload,
)
@@ -58,19 +60,22 @@ class TestIsUserFacingLxmfPayload:
assert not is_user_facing_lxmf_payload({}, None, None)
assert not is_user_facing_lxmf_payload({}, " ", " \t\n")
- def test_reaction_via_app_extensions_string_keys(self):
- fields = {"app_extensions": {"reaction_to": "abc", "emoji": "fire"}}
+ def test_reaction_via_parsed_reaction_field(self):
+ fields = {"reaction": {"reaction_to": "abc", "reaction_content": "fire"}}
assert not is_user_facing_lxmf_payload(fields, "", "")
assert not is_user_facing_lxmf_payload(fields, None, None)
- def test_reaction_via_raw_int_field(self):
- fields = {LXMF_APP_EXTENSIONS_FIELD: {"reaction_to": "abc"}}
+ def test_reaction_via_raw_lxmf_field(self):
+ fields = {
+ FIELD_REACTION: {
+ REACTION_TO: bytes.fromhex("ab" * 16),
+ REACTION_CONTENT: b"fire",
+ },
+ }
assert not is_user_facing_lxmf_payload(fields, "", "")
def test_reaction_with_text_is_still_not_user_facing(self):
- # A reaction payload that also carries content is still a reaction
- # event; we never raise the bell on reactions.
- fields = {"app_extensions": {"reaction_to": "abc"}}
+ fields = {"reaction": {"reaction_to": "abc", "reaction_content": "\U0001f44d"}}
assert not is_user_facing_lxmf_payload(fields, "noise", "noise")
def test_telemetry_only_is_not_user_facing(self):
@@ -112,7 +117,9 @@ class TestIsUserFacingLxmfPayload:
assert not is_user_facing_lxmf_payload(fields, "", "")
def test_accepts_json_string_fields(self):
- fields = json.dumps({"app_extensions": {"reaction_to": "abc"}})
+ fields = json.dumps(
+ {"reaction": {"reaction_to": "abc", "reaction_content": "\U0001f44d"}}
+ )
assert not is_user_facing_lxmf_payload(fields, "", "")
fields = json.dumps({"image": {"image_size": 1}})
assert is_user_facing_lxmf_payload(fields, "", "")
@@ -149,7 +156,9 @@ class TestRequireUserFacingFlag:
def test_legacy_behavior_unchanged_without_flag(self):
row = _row(
incoming=1,
- fields={"app_extensions": {"reaction_to": "abc"}},
+ fields={
+ "reaction": {"reaction_to": "abc", "reaction_content": "\U0001f44d"}
+ },
)
# Without ``require_user_facing`` the helper preserves its old behavior
# so the conversation list (which renders reactions) is unaffected.
@@ -158,7 +167,9 @@ class TestRequireUserFacingFlag:
def test_reaction_filtered_when_require_user_facing(self):
row = _row(
incoming=1,
- fields={"app_extensions": {"reaction_to": "abc"}},
+ fields={
+ "reaction": {"reaction_to": "abc", "reaction_content": "\U0001f44d"}
+ },
)
assert (
compute_lxmf_conversation_unread_from_latest_row(
@@ -311,7 +322,9 @@ class TestGetLatestUserFacingIncomingMessage:
msg_hash="h2",
peer_hash=PEER_HASH,
content="",
- fields={"app_extensions": {"reaction_to": "abc"}},
+ fields={
+ "reaction": {"reaction_to": "abc", "reaction_content": "\U0001f44d"}
+ },
timestamp=200,
),
)
@@ -393,7 +406,12 @@ class TestGetLatestUserFacingIncomingMessage:
msg_hash=f"react{i}",
peer_hash=PEER_HASH,
content="",
- fields={"app_extensions": {"reaction_to": "x"}},
+ fields={
+ "reaction": {
+ "reaction_to": "x",
+ "reaction_content": "\U0001f44d",
+ }
+ },
timestamp=100 + i,
),
)
@@ -493,7 +511,7 @@ class TestNotificationsGetUserFacingFilter:
msg_hash="r1",
peer_hash=PEER_HASH,
content="",
- fields={"app_extensions": {"reaction_to": "abc", "emoji": "fire"}},
+ fields={"reaction": {"reaction_to": "abc", "reaction_content": "fire"}},
timestamp=1_700_000_000,
),
)
@@ -522,7 +540,9 @@ class TestNotificationsGetUserFacingFilter:
msg_hash="r1",
peer_hash=PEER_HASH,
content="",
- fields={"app_extensions": {"reaction_to": "m1"}},
+ fields={
+ "reaction": {"reaction_to": "m1", "reaction_content": "\U0001f44d"}
+ },
timestamp=1_700_001_000,
),
)
@@ -549,7 +569,9 @@ class TestNotificationsGetUserFacingFilter:
msg_hash="r1",
peer_hash=PEER_HASH,
content="",
- fields={"app_extensions": {"reaction_to": "m1"}},
+ fields={
+ "reaction": {"reaction_to": "m1", "reaction_content": "\U0001f44d"}
+ },
timestamp=1_700_001_000,
),
)
@@ -674,7 +696,9 @@ class TestNotificationsGetUserFacingFilter:
msg_hash="m2",
peer_hash=PEER_HASH_2,
content="",
- fields={"app_extensions": {"reaction_to": "x"}},
+ fields={
+ "reaction": {"reaction_to": "x", "reaction_content": "\U0001f44d"}
+ },
timestamp=1_700_000_200,
),
)
@@ -699,7 +723,9 @@ class TestNotificationsGetUserFacingFilter:
msg_hash="r1",
peer_hash=PEER_HASH,
content="",
- fields={"app_extensions": {"reaction_to": "abc"}},
+ fields={
+ "reaction": {"reaction_to": "abc", "reaction_content": "\U0001f44d"}
+ },
timestamp=1_700_000_000,
),
)

diff --git a/tests/frontend/MicronEditorPage.test.js b/tests/frontend/MicronEditorPage.test.js
index ae02bca1..b5714f78 100644
--- a/tests/frontend/MicronEditorPage.test.js
+++ b/tests/frontend/MicronEditorPage.test.js
@@ -4,6 +4,21 @@ import MicronEditorPage from "@/components/micron-editor/MicronEditorPage.vue";
import { micronStorage } from "@/js/MicronStorage";
import DialogUtils from "@/js/DialogUtils";
+const micronEditorT = (key, params = {}) => {
+ const strings = {
+ "tools.micron_editor.new_tab": "New Tab",
+ "tools.micron_editor.publish_prompt_name":
+ 'index.mu already exists on "{server}". Enter a page name (without .mu):',
+ "tools.micron_editor.publish_published": 'Published "{page}" to {server}',
+ "tools.micron_editor.publish_failed": "Failed to publish page",
+ };
+ let out = strings[key] ?? key;
+ for (const [k, v] of Object.entries(params)) {
+ out = out.replace(`{${k}}`, String(v));
+ }
+ return out;
+};
+
vi.mock("@/js/MicronStorage", () => ({
micronStorage: {
loadTabs: vi.fn().mockResolvedValue([]),
@@ -15,6 +30,8 @@ vi.mock("@/js/MicronStorage", () => ({
vi.mock("@/js/DialogUtils", () => ({
default: {
confirm: vi.fn(),
+ alert: vi.fn(),
+ prompt: vi.fn(),
},
}));
@@ -32,11 +49,11 @@ describe("MicronEditorPage.vue", () => {
});
});
- const mountMicronEditorPage = () => {
+ const mountMicronEditorPage = (t = micronEditorT) => {
return mount(MicronEditorPage, {
global: {
mocks: {
- $t: (key) => key,
+ $t: t,
},
stubs: {
MaterialDesignIcon: {
@@ -80,6 +97,55 @@ describe("MicronEditorPage.vue", () => {
expect(wrapper.find(".nodeContainer").text()).toContain("TestContent");
});
+ it("isUnsetMicronTabName matches default new tab labels", async () => {
+ const wrapper = mountMicronEditorPage();
+ await vi.waitFor(() => expect(wrapper.vm.tabs.length).toBeGreaterThan(0));
+ expect(wrapper.vm.isUnsetMicronTabName("New Tab 2")).toBe(true);
+ expect(wrapper.vm.isUnsetMicronTabName("Homepage")).toBe(false);
+ expect(wrapper.vm.isUnsetMicronTabName("Main")).toBe(false);
+ });
+
+ it("resolvePublishPageBase uses index when server has no index.mu", async () => {
+ const wrapper = mountMicronEditorPage();
+ await vi.waitFor(() => expect(wrapper.vm.tabs.length).toBeGreaterThan(0));
+ const tab = { name: "New Tab 1", content: "x" };
+ await expect(wrapper.vm.resolvePublishPageBase(tab, [], "srv")).resolves.toBe("index");
+ });
+
+ it("resolvePublishPageBase uses tab name when index.mu exists and tab is renamed", async () => {
+ const wrapper = mountMicronEditorPage();
+ await vi.waitFor(() => expect(wrapper.vm.tabs.length).toBeGreaterThan(0));
+ const tab = { name: "About Page", content: "x" };
+ await expect(wrapper.vm.resolvePublishPageBase(tab, ["index.mu"], "srv")).resolves.toBe("About_Page");
+ });
+
+ it("resolvePublishPageBase prompts when index.mu exists and tab name is unset", async () => {
+ DialogUtils.prompt.mockResolvedValue("custom_page");
+ const wrapper = mountMicronEditorPage();
+ await vi.waitFor(() => expect(wrapper.vm.tabs.length).toBeGreaterThan(0));
+ const tab = { name: "New Tab 1", content: "x" };
+ await expect(wrapper.vm.resolvePublishPageBase(tab, ["index.mu"], "srv")).resolves.toBe("custom_page");
+ expect(DialogUtils.prompt).toHaveBeenCalled();
+ });
+
+ it("publishToNode posts index.mu when server has no index page", async () => {
+ window.api = {
+ get: vi.fn().mockResolvedValue({ data: { pages: [] } }),
+ post: vi.fn().mockResolvedValue({ data: { name: "index.mu" } }),
+ };
+ const wrapper = mountMicronEditorPage();
+ await vi.waitFor(() => expect(wrapper.vm.tabs.length).toBeGreaterThan(0));
+ await wrapper.setData({
+ tabs: [{ id: 1, name: "New Tab 1", content: "hello" }],
+ activeTabIndex: 0,
+ });
+ await wrapper.vm.publishToNode({ node_id: "n1", name: "My Server" });
+ expect(window.api.post).toHaveBeenCalledWith("/api/v1/page-nodes/n1/pages", {
+ name: "index",
+ content: "hello",
+ });
+ });
+
it("resets all content", async () => {
DialogUtils.confirm.mockResolvedValue(true);
const wrapper = mountMicronEditorPage();

diff --git a/tests/frontend/NotificationBell.test.js b/tests/frontend/NotificationBell.test.js
index 186a8b8e..9457c9ed 100644
--- a/tests/frontend/NotificationBell.test.js
+++ b/tests/frontend/NotificationBell.test.js
@@ -315,14 +315,14 @@ describe("NotificationBell false-trigger suppression", () => {
is_incoming: true,
is_reaction: true,
content: "",
- fields: { app_extensions: { reaction_to: "abc", emoji: "fire" } },
+ fields: { reaction: { reaction_to: "abc", reaction_content: "fire" } },
},
});
await new Promise((r) => setTimeout(r, 30));
expectNoNotificationsReload(before);
});
- it("does NOT reload on inbound reaction signaled only via app_extensions", async () => {
+ it("does NOT reload on inbound reaction signaled only via fields.reaction", async () => {
const wrapper = mountBell();
await wrapper.vm.$nextTick();
const before = global.api.get.mock.calls.filter((c) => c[0] === "/api/v1/notifications").length;
@@ -331,7 +331,7 @@ describe("NotificationBell false-trigger suppression", () => {
lxmf_message: {
is_incoming: true,
content: "",
- fields: { app_extensions: { reaction_to: "abc" } },
+ fields: { reaction: { reaction_to: "abc", reaction_content: "\u{1F44D}" } },
},
});
await new Promise((r) => setTimeout(r, 30));
@@ -492,7 +492,7 @@ describe("NotificationBell false-trigger suppression", () => {
fn({
is_incoming: true,
content: "",
- fields: { app_extensions: { reaction_to: "x" } },
+ fields: { reaction: { reaction_to: "x", reaction_content: "\u{1F44D}" } },
})
).toBe(false);
expect(fn({ is_incoming: true, content: "", fields: { telemetry: { x: 1 } } })).toBe(false);
@@ -516,7 +516,7 @@ describe("NotificationBell false-trigger suppression", () => {
lxmf_message: {
is_incoming: true,
content: "",
- fields: { app_extensions: { reaction_to: `m${i}` } },
+ fields: { reaction: { reaction_to: `m${i}`, reaction_content: "\u{1F44D}" } },
},
});
}

diff --git a/tests/frontend/lxmfConversationPreview.test.js b/tests/frontend/lxmfConversationPreview.test.js
index 6a4cfa08..d6e2b525 100644
--- a/tests/frontend/lxmfConversationPreview.test.js
+++ b/tests/frontend/lxmfConversationPreview.test.js
@@ -33,12 +33,12 @@ describe("lxmfConversationListPreview", () => {
expect(s).toBe("Alex reacted \u2764\uFE0F");
});
- it("reads emoji from fields.app_extensions when body fields are used", () => {
+ it("reads emoji from fields.reaction when body fields are used", () => {
const s = lxmfConversationListPreview(
{
content: "",
is_incoming: true,
- fields: { app_extensions: { reaction_to: "deadbeef", emoji: "\u{1F602}" } },
+ fields: { reaction: { reaction_to: "deadbeef", reaction_content: "\u{1F602}" } },
},
{ myLxmfAddressHash: me, peerDisplayName: "Sam" }
);

diff --git a/tests/frontend/lxmfReactions.test.js b/tests/frontend/lxmfReactions.test.js
index d6a74c1c..cc14c697 100644
--- a/tests/frontend/lxmfReactions.test.js
+++ b/tests/frontend/lxmfReactions.test.js
@@ -1,5 +1,23 @@
import { describe, expect, it } from "vitest";
-import { mergeLxmfReactionRowsIntoMessages } from "../../meshchatx/src/frontend/js/lxmfReactions";
+import {
+ mergeLxmfReactionRowsIntoMessages,
+ reactionEmojiFromLxmfMessageFields,
+} from "../../meshchatx/src/frontend/js/lxmfReactions";
+
+describe("reactionEmojiFromLxmfMessageFields", () => {
+ it("reads standard LXMF reaction field", () => {
+ expect(
+ reactionEmojiFromLxmfMessageFields({
+ reaction: { reaction_to: "a".repeat(32), reaction_content: "\u{1F44D}" },
+ })
+ ).toBe("\u{1F44D}");
+ });
+
+ it("returns empty when reaction field is absent", () => {
+ expect(reactionEmojiFromLxmfMessageFields({})).toBe("");
+ expect(reactionEmojiFromLxmfMessageFields({ app_extensions: { pending: true } })).toBe("");
+ });
+});
describe("mergeLxmfReactionRowsIntoMessages", () => {
it("returns an empty array when input is not an array", () => {

diff --git a/tests/frontend/mapTileProviders.test.js b/tests/frontend/mapTileProviders.test.js
new file mode 100644
index 00000000..59d7e691
--- /dev/null
+++ b/tests/frontend/mapTileProviders.test.js
@@ -0,0 +1,19 @@
+import { describe, expect, it } from "vitest";
+import {
+ detectRasterTileProviderId,
+ nextRasterTileProviderId,
+ RASTER_TILE_PROVIDER_ORDER,
+} from "../../meshchatx/src/frontend/js/mapTileProviders.js";
+
+describe("mapTileProviders", () => {
+ it("detects provider from URL", () => {
+ expect(detectRasterTileProviderId("https://tiles.openfreemap.org/styles/bright")).toBe("openfreemap");
+ expect(detectRasterTileProviderId("https://tile.openstreetmap.org/1/1/1.png")).toBe("osm");
+ });
+
+ it("returns next provider not yet attempted", () => {
+ expect(nextRasterTileProviderId("openfreemap", [])).toBe("carto-voyager");
+ expect(nextRasterTileProviderId("openfreemap", ["carto-voyager"])).toBe("osm");
+ expect(nextRasterTileProviderId("carto-light", RASTER_TILE_PROVIDER_ORDER)).toBe(null);
+ });
+});


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────